package ua.edu.nuos.jeetraining2012.cms.model;

import org.slf4j.Logger;
import ua.edu.nuos.jeetraining2012.cms.dto.predefined.StandardUserStatuses;
import ua.edu.nuos.jeetraining2012.cms.entity.UsersToCourseEntity;
import ua.edu.nuos.jeetraining2012.cms.entity.UsersToCourseId;
import ua.edu.nuos.jeetraining2012.cms.model.exception.DAOException;

import javax.ejb.Local;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;

/**
 * @author Victor N. Ganichev victor.ganichev@gmail.com
 * @version 1.0 Date: 17.06.13 Time: 14:17
 * @since 1.0
 */
@Local
@Stateless
public class StudentsToCoursesDAO {

    @PersistenceContext
    private EntityManager em;

    /**
     * Define logger.
     */
    final private static Logger logger = org.slf4j.LoggerFactory.getLogger(StudentsToCoursesDAO.class);

    // Getters & Setters
    protected EntityManager getEm() {
        return em;
    }

    private void setEm(EntityManager em) {
        this.em = em;
    }

// Constructors

    /**
     * Default constructor.
     */
    public StudentsToCoursesDAO() {
        logger.debug("StudentDAO start.");
    }

    /**
     * Constructor with customized EntityManager. Use this constructor for testing purposes.
     *
     * @param em Instance of EntityManager.
     */
    public StudentsToCoursesDAO(EntityManager em) {
        logger.debug("StudentDAO start with EntityManager {}", em);
        setEm(em);
    }

    // Actions

    /**
     * Create relation students & courses. E.i. enroll student to course.
     * <p/>
     * Return <code>true</code> if success added.
     *
     * @param dto DTO with predefined student id and course id.
     * @return True if record added.
     * @throws IllegalArgumentException If dto is null.
     * @throws DAOException             If parent exception appeared.
     */
    public boolean create(UsersToCourseEntity dto) throws IllegalArgumentException, DAOException {
        logger.debug("create() - started.");

        if (dto == null) {
            logger.error("DTO can't be null");
            logger.debug("create() - finished with IllegalArgumentException.");
            throw new IllegalArgumentException("DTO can't be null.");
        }

        boolean isPersist = false;
        logger.debug("Creating record with dto {}", dto);
        try {
            em.persist(dto);
            isPersist = true;
            logger.info("Record successful created.");
        } catch (Exception e) {
            logger.error("Can't add record into DB. Error: {} \nTrace:\n{}", e.getMessage(),
                    Arrays.deepToString(e.getStackTrace()));
            logger.debug("create() - finished with DAOException.");
            throw new DAOException("Can't add record. Error: " + e.getMessage());
        }

        logger.debug("create() - finished.");
        return isPersist;
    }

    /**
     * Remove student to course relation. E.i. un-enroll student from course.
     * <p/>
     * In case of success, return true.
     *
     * @param id Complex ID with course ID & student ID.
     * @return True, if record removed.
     * @throws IllegalArgumentException If id is null or courseId less that 1 or userId null or empty.
     * @throws DAOException             If parent Exception appeared.
     */
    public boolean delete(UsersToCourseId id) throws IllegalArgumentException, DAOException {
        logger.debug("delete() - started.");

        if (id == null || id.getCourseId() < 1 || id.getUserId().isEmpty() || id.getUserId() == null) {
            logger.error("ID can't be null");
            logger.debug("delete() - finished with IllegalArgumentException.");
            throw new IllegalArgumentException("ID can't be null");
        }

        boolean isDeleted = false;
        logger.debug("Deleting record.");
        try {
            logger.debug("Trying to find entity with ID {}", id);
            UsersToCourseEntity dto = em.find(UsersToCourseEntity.class, id);
            em.remove(dto);
            isDeleted = true;
            logger.info("Entity removed.");
        } catch (Exception e) {
            logger.error("Can't remove record from DB. Error: {} \nTrace:\n {}", e.getMessage(),
                    Arrays.deepToString(e.getStackTrace()));
            logger.debug("delete() - finished with DAOException.");
            throw new DAOException("Can't remove record. Error message: " + e.getMessage());
        }

        logger.debug("delete() - finished.");
        return isDeleted;
    }

    /**
     * Wrapper for {@link StudentsToCoursesDAO#findCoursesIdByUserId(String, String)} with default status {@link
     * StandardUserStatuses#ENROLLED}
     *
     * @param userId User ID.
     * @return List of UserToCourseEntity-es.
     * @throws DAOException             If any parent exception appeared.
     * @throws IllegalArgumentException If userId null or empty.
     */
    public List<Integer> findCoursesIdByUserId(String userId) throws DAOException, IllegalArgumentException {
        return findCoursesIdByUserId(userId, null);
    }

    /**
     * Return list of course IDs, where student with userId was enrolled or in pending status. If no userStatus was
     * sent, {@link StandardUserStatuses#ENROLLED} status will be uses by default.
     *
     * @param userId User ID.
     * @return List of UserToCourseEntity-es.
     * @throws DAOException             If any parent exception appeared.
     * @throws IllegalArgumentException If userId null or empty.
     */
    public List<Integer> findCoursesIdByUserId(String userId, String userStatus) throws DAOException, IllegalArgumentException {
        logger.debug("findCoursesByUserId() - started.");

        if (userId == null || userId.isEmpty()) {
            logger.error("User ID can't be {}", userId);
            logger.debug("findCoursesByUserId() - finished with IllegalArgumentException.");
            throw new IllegalArgumentException("User ID can't be null or empty.");
        }

        if (userStatus == null || userStatus.isEmpty()) {
            logger.info("No user status were sent, default: {} will be used.", StandardUserStatuses.ENROLLED);
            userStatus = StandardUserStatuses.ENROLLED;
        }

        List<Integer> courses = null;
        logger.debug("findCoursesByUserId() Trying to find courses for user with ID {}.", userId);
        try {
            final String sql = "SELECT u2c.course_id FROM users_to_courses u2c WHERE u2c.user_id = ?1 AND u2c.student_status = ?2";
            Query q = em.createNativeQuery(sql).setParameter(1, userId).setParameter(2, userStatus);
            logger.debug("findCoursesByUserId() - Querying DB with Query: {}", q);

            courses = q.getResultList();
            if (courses != null) {
                logger.info("findCoursesByUserId() Found {} courses.", courses.size());
            }
        } catch (Exception e) {
            logger.error("Can't find courses. Error message: {} \nTrace:\n {}", e.getMessage(),
                    Arrays.deepToString(e.getStackTrace()));
            logger.debug("findCoursesByUserId() finished with DAOException.");
            throw new DAOException("Can't find courses. Error message: " + e.getMessage());
        }

        logger.debug("findCoursesByUserId() - finished.");
        return courses;
    }

    /**
     * Wrapper for {@link StudentsToCoursesDAO#countCoursesByUserId(String, String)} with default
     * value in userStatus {@link StandardUserStatuses#ENROLLED}
     *
     * @param userId User ID.
     * @return Number users enrolled courses.
     * @throws IllegalArgumentException If userId is null or empty.
     * @throws DAOException             If any parent exception appeared.
     */
    public Long countCoursesByUserId(String userId) throws IllegalArgumentException, DAOException {
        return countCoursesByUserId(userId, null);
    }

    /**
     * Count courses by user ID. <p/> In case of success, return number of courses, where student had been enrolled (0
     * or more). If no studentStatus specified, {@link StandardUserStatuses#ENROLLED} status will be used by default.
     * </p> If userId is null or empty, IllegalArgumentException will be thrown.<br/> If any parent exception appeared,
     * during querying, DAOException will be thrown.
     *
     * @param userId User ID.
     * @return Number users enrolled courses.
     * @throws IllegalArgumentException If userId is null or empty.
     * @throws DAOException             If any parent exception appeared.
     */
    public Long countCoursesByUserId(String userId, String studentStatus) throws IllegalArgumentException, DAOException {
        logger.debug("countCoursesByUserId() - started.");

        if (userId == null || userId.isEmpty()) {
            logger.error("User ID can't be {}", userId);
            logger.debug("countCoursesByUserId - finished with IllegalArgumentException.");
            throw new IllegalArgumentException("User id can't be null or empty.");
        }

        if (studentStatus == null || studentStatus.isEmpty()) {
            logger.info("No student status was sent, {} will be user by default.", StandardUserStatuses.ENROLLED);
            studentStatus = StandardUserStatuses.ENROLLED;
        }

        Long courseCounter = 0l;
        logger.debug("Trying to count users courses.");
        try {
            final String sql = "SELECT COUNT(course_id) FROM users_to_courses WHERE user_id = ?1 AND student_status = ?2";
            Query q = em.createNativeQuery(sql).setParameter(1, userId).setParameter(2, studentStatus);
            courseCounter = (Long) q.getSingleResult();
            logger.info("Found {} courses.", courseCounter);
        } catch (NoResultException e) {
            logger.debug("Can't find any course.");
            courseCounter = 0l;
        } catch (Exception e) {
            logger.error("Can't count users courses. Error mesage: {}, \nTrace:\n {}", e.getMessage(),
                    e.getStackTrace());
            logger.debug("countCourseByUserId - finished with DAOException.");
            throw new DAOException("The number of students courses can't be counted. Got Error: " + e.getMessage());
        }

        logger.debug("countCoursesByUserId() - finished.");
        return courseCounter;
    }

    /**
     * Return MD5 checksum for approve link.
     * <p/>
     * In case of success, return MD5 checksum for user_id + course_id + join_date or <code>null</code>
     * if checksum can't be calculated.
     * <p/>
     * If user ID is null or empty IllegalArgumentException will be thrown.
     * If course ID is null or less than 1, IllegalArgumentException will be thrown.
     * If any parent exception appeared, DAOException will be thrown.
     *
     * @param userId                        User ID.
     * @param courseId                      Course ID.
     * @return                              String checksum or null.
     * @throws IllegalArgumentException     If user ID is null or empty.
     *                                      If course ID is null or less than 1.
     * @throws DAOException                 If any parent exception appeared.
     */
    public String getApproveMD5(String userId, Long courseId) throws IllegalArgumentException, DAOException {
        logger.debug("getApproveMD5() - started.");

        if (userId == null || userId.isEmpty()) {
            logger.error("user ID can't be {}", userId);
            logger.debug("getApproveMD5 - finished with IllegalArgumentException.");
            throw new IllegalArgumentException("User ID is null or empty.");
        }

        if (courseId == null || courseId < 1) {
            logger.error("Course ID can't be {}", courseId);
            logger.debug("getApproveMD5() - finished with IllegalArgumentException.");
            throw new IllegalArgumentException("Course ID can't be null or less than 1.");
        }

        String md5 = null;
        try {
            final String sql = "SELECT MD5(\"course_id\" || \"user_id\" ) AS checksum " +
                    "FROM users_to_courses WHERE course_id = ?1 AND user_id = ?2";
            Query q = em.createNativeQuery(sql).setParameter(1, courseId).setParameter(2, userId);
            md5 = (String) q.getSingleResult();
            logger.info("Approve checksum {}", md5);
        } catch (NoResultException e) {
            logger.info("No result was found.");
        } catch (Exception e) {
            logger.error("Can't calc checksum. Error message: {}, \nTrace:\n {}", e.getMessage(),
                    e.getStackTrace());
            logger.debug("getApproveMD5() - finished with DAOException.");
            throw new DAOException("Can't calculate checksum. Error message: " + e.getMessage());
        }

        logger.debug("getApproveMD5() - finished.");
        return md5;
    }

    /**
     * Find users to course relation by MD5.
     * <p/>
     * In case of success, return filled {@link UsersToCourseEntity}, either <code>null</code>.
     * <p/>
     * If md5 is <code>null</code> or empty, IllegalArgumentException will be thrown.<br/>
     * If any parent Exception appeared during find, {@link DAOException} will be thrown.
     *
     * @param md5                       Hash.
     * @return                          Instance of UserToCourseEntity.
     * @throws DAOException             If any exception appeared during find.
     * @throws IllegalArgumentException If md5 is null or empty.
     */
    public UsersToCourseEntity findByMd5(String md5) throws DAOException, IllegalArgumentException {
        logger.debug("findByMd5() - started.");

        if (md5 == null || md5.isEmpty()) {
            logger.error("Hash can't be {}", md5);
            logger.debug("findByMd5() - finished with IllegalArgumentException.");
            throw new IllegalArgumentException("Hash can't be null or empty");
        }

        UsersToCourseEntity entity = null;
        try {
            final String sql = "SELECT * FROM users_to_courses WHERE MD5(\"course_id\" || \"user_id\") = ?1";
            Query q = em.createNativeQuery(sql, UsersToCourseEntity.class).setParameter(1, md5);
            entity = (UsersToCourseEntity) q.getSingleResult();
        } catch (NoResultException e) {
            logger.info("No result was found.");
        } catch (Exception e) {
            logger.error("Can't find entity. Got error: {} \nTrace:\n {}", e.getMessage(), e.getStackTrace());
            logger.debug("findByMd5() - finished with DAOException.");
            throw new DAOException("Can't find Entity. Error: {}" + e.getMessage());
        }

        logger.debug("findByMd5() - finished.");
        return entity;
    }

    /**
     * Change student status to {@link StandardUserStatuses#ENROLLED}.
     *
     * In case of success, return {@link UsersToCourseEntity}.
     *
     * @param md5                           Hash of record to be changed.
     * @return                              Instance of UsersToCoursesEntity.
     * @throws IllegalArgumentException     If md5 is null or empty.
     * @throws DAOException                 If any exception appeared during change.
     */
    public UsersToCourseEntity changeStudentStatus(String md5) throws DAOException {
        return changeStudentStatus(md5, null);
    }

    /**
     * Similar to {@link StudentsToCoursesDAO#changeStudentStatus(String)}, except, student status will be
     * changed to <code>status</code>.
     *
     * @param md5                           Hash of record to be changed.
     * @param status                        New record status. One of {@link StandardUserStatuses}
     * @return                              Instance of UsersToCourseEntity.
     * @throws IllegalArgumentException     If hash is null or empty.
     * @throws DAOException                 If any parent exception appeared.
     */
    public UsersToCourseEntity changeStudentStatus(String md5, String status) throws IllegalArgumentException, DAOException {
        logger.debug("changeStudentStatus() - started.");

        if (md5 == null || md5.isEmpty()) {
            logger.error("MD5 can't be {}", md5);
            logger.debug("changeStudentStatus() - finished with IllegalArgumentException");
            throw new IllegalArgumentException("Hash can't be null or empty");
        }

        if (status == null || status.isEmpty()) {
            logger.error("Status is {}. Default status {} will be used.", status, StandardUserStatuses.ENROLLED);
            status = StandardUserStatuses.ENROLLED;
        }

        UsersToCourseEntity entity = null;
        try {
            entity = findByMd5(md5);
            if (entity != null) {
                entity.setStatus(status);
                em.merge(entity);
                em.flush();
            }
        } catch (Exception e) {
            logger.error("Can't update entity status. Message: {}, \nTrace:\n {}", e.getMessage(),
                    e.getStackTrace());
            logger.debug("changeStudentStatus() - finished with DAOException.");
            throw new DAOException("Can't update user status. Error: " + e.getMessage());
        }

        logger.debug("changeStudentStatus() - finished.");
        return entity;
    }

    public boolean changeStudentSvnLink(UsersToCourseEntity u2c) throws IllegalArgumentException, DAOException {
        logger.debug("changeStudentSvnLink() - started.");

        if (u2c == null) {
            logger.error("User 2 Course relation can't be null.");
            logger.debug("changeStudentSvnLink() - finished with IllegalArgumentException.");
            throw new IllegalArgumentException("User to course relation can't be null");
        }

        boolean isUpdated = false;
        try {
            em.merge(u2c);
            isUpdated = true;
        } catch (Exception e) {
            logger.error("Can't update entity. Error: {}, \nTrace:\n {}", e.getMessage(), e.getStackTrace());
            logger.debug("changeStudentSvnLink() - finished with DAOException.");
            throw new DAOException("Can't update entity. Error: {}" + e.getMessage());
        }

        logger.debug("changeStudentSvnLink() - finished.");
        return isUpdated;
    }

    public UsersToCourseEntity findByUsersToCourseId(UsersToCourseId usersToCourseId) throws DAOException {
        logger.debug("findByUsersToCourseId() - invocation");

        if(usersToCourseId == null){
            logger.error("UsersToCourseId can't be {}", usersToCourseId);
            logger.debug("findByUsersToCourseId() - finished with IllegalArgumentException");
            throw new IllegalArgumentException("UsersToCourseId can't be null");
        }

        UsersToCourseEntity entity = null;
        try {

            Query q = em.createQuery("SELECT c FROM UsersToCourseEntity c " +
                    "WHERE c.courseId.id = :courseId and  c.userId.id = :userId");
            q.setParameter("courseId", usersToCourseId.getCourseId());
            q.setParameter("userId", usersToCourseId.getUserId());

            logger.debug("Trying to find with query: {}. Bound with params userId: {}, courseId: {}", q,
                    usersToCourseId.getUserId(), usersToCourseId.getCourseId());

            entity = (UsersToCourseEntity) q.getSingleResult();
        } catch (NoResultException e) {
            logger.info("No result was found.");
        } catch (Exception e) {
            logger.error("Can't find entity. Got error: {} \nTrace:\n {}", e.getMessage(), e.getStackTrace());
            logger.debug("findByUsersToCourseId() - finished with DAOException.");
            throw new DAOException("Can't find Entity. Error: {}" + e.getMessage());
        }

        logger.debug("findByUsersToCourseId() - finish with {}", entity);
        return entity;
    }

    /**
     * Get course students with any status
     *
     * @param courseId Course Id to find assigned users.
     * @return         List of users attached to course
     * @throws DAOException If any parent exception appeared
     * @throws IllegalArgumentException courseId is null
     */
    public List<UsersToCourseEntity> findStudentsByCourse(Long courseId) throws DAOException {
        logger.debug("findStudentsByCourse() - started.");

        if(courseId == null){
            logger.debug("findStudentsByCourse() - finished with IllegalArgumentException");
            throw new IllegalArgumentException("UsersToCourseId can't be null");
        }

        try {
            final String sql = "SELECT c FROM UsersToCourseEntity c WHERE c.courseId.id = :courseId";
            return em.createQuery(sql, UsersToCourseEntity.class).setParameter("courseId", courseId).getResultList();
        } catch (NoResultException e) {
            logger.info("No result was found.");
        } catch (Exception e) {
            logger.error("Can't find entity. Got error: {} \nTrace:\n {}", e.getMessage(), e.getStackTrace());
            logger.debug("findStudentsByCourse() - finished with DAOException.");
            throw new DAOException("Can't find Entity. Error: {}" + e.getMessage());
        }

        logger.debug("findStudentsByCourse() - finished.");
        return new LinkedList();
    }
}
